﻿' ******************************************************************************
' ** 
' **  Yahoo! Managed
' **  Written by Marius Häusler 2011
' **  It would be pleasant, if you contact me when you are using this code.
' **  Contact: YahooFinanceManaged@gmail.com
' **  Project Home: http://code.google.com/p/yahoo-finance-managed/
' **  
' ******************************************************************************
' **  
' **  Copyright 2011 Marius Häusler
' **  
' **  Licensed under the Apache License, Version 2.0 (the "License");
' **  you may not use this file except in compliance with the License.
' **  You may obtain a copy of the License at
' **  
' **    http://www.apache.org/licenses/LICENSE-2.0
' **  
' **  Unless required by applicable law or agreed to in writing, software
' **  distributed under the License is distributed on an "AS IS" BASIS,
' **  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
' **  See the License for the specific language governing permissions and
' **  limitations under the License.
' ** 
' ******************************************************************************


Namespace YahooManaged.Weather.API


    Public Class WeatherFeedDownload
        Inherits Base.Download

        Public Event AsyncDownloadCompleted(ByVal sender As Base.Download, ByVal ea As WeatherFeedDownloadCompletedEventArgs)

        Private mXmlParser As New RSS.ImportExport.XML

        Public Function Download(ByVal woeids As IEnumerable(Of Long), ByVal metricValues As Boolean) As WeatherFeedResponse
            If woeids Is Nothing Then : Throw New ArgumentNullException("woeids", "The passed list is null.")
            Else : Return Me.Download(Me.DownloadUrl(woeids, metricValues))
            End If
        End Function
        Public Function Download(ByVal locations As IEnumerable(Of LocationID), ByVal metricValues As Boolean) As WeatherFeedResponse
            Return Me.Download(Me.DownloadUrl(locations, metricValues))
        End Function
        Public Function Download(ByVal keyword As String, ByVal metricValues As Boolean, ByVal opt As ResultCountOptions) As WeatherFeedResponse
            Return Me.Download(New String() {keyword}, metricValues, opt)
        End Function
        Public Function Download(ByVal keywords As IEnumerable(Of String), ByVal metricValues As Boolean, ByVal opt As ResultCountOptions) As WeatherFeedResponse
            Return Me.Download(Me.DownloadUrl(keywords, opt, metricValues))
        End Function
        Private Function Download(ByVal url As String) As WeatherFeedResponse
            Return Me.ToResponse(MyBase.DownloadStream(url))
        End Function


        Public Overloads Sub DownloadAsync(ByVal woeids As IEnumerable(Of Long), ByVal metricValues As Boolean, Optional ByVal userArgs As Object = Nothing)
            If woeids Is Nothing Then : Throw New ArgumentNullException("woeids", "The passed list is null.")
            Else
                Dim args As New AsyncDownloadArgs(userArgs) With {.WOEIDs = mHelper.EnumToArray(woeids)}
                MyBase.DownloadStreamAsync(Me.DownloadUrl(args.WOEIDs, metricValues), args)
            End If
        End Sub
        Public Overloads Sub DownloadAsync(ByVal locations As IEnumerable(Of LocationID), ByVal metricValues As Boolean, Optional ByVal userArgs As Object = Nothing)
            If locations Is Nothing Then : Throw New ArgumentNullException("locations", "The passed list is null.")
            Else
                Dim args As New AsyncDownloadArgs(userArgs) With {.Locations = mHelper.EnumToArray(locations)}
                MyBase.DownloadStreamAsync(Me.DownloadUrl(args.Locations, metricValues), args)
            End If
        End Sub
        Public Overloads Sub DownloadAsync(ByVal keyword As String, ByVal metricValues As Boolean, ByVal opt As ResultCountOptions, Optional ByVal userArgs As Object = Nothing)
            Me.DownloadAsync(New String() {keyword}, metricValues, opt)
        End Sub
        Public Overloads Sub DownloadAsync(ByVal keywords As IEnumerable(Of String), ByVal metricValues As Boolean, ByVal opt As ResultCountOptions, Optional ByVal userArgs As Object = Nothing)
            If keywords Is Nothing Then : Throw New ArgumentNullException("keywords", "The passed list is null.")
            Else
                Dim args As New AsyncDownloadArgs(userArgs) With {.Keywords = mHelper.EnumToArray(keywords), .Options = New ResultCountOptions(opt)}
                MyBase.DownloadStreamAsync(Me.DownloadUrl(args.Keywords, args.Options, metricValues), args)
            End If
        End Sub


        ''' <summary>
        ''' Default constructor
        ''' </summary>
        ''' <remarks></remarks>
        Public Sub New()
        End Sub


        Private Sub DownloadAsync_Completed(ByVal sender As Base.Download, ByVal e As Base.StreamDownloadCompletedEventArgs) Handles MyBase.AsyncStreamDownloadCompleted
            Dim dlArgs As AsyncDownloadArgs = DirectCast(e.UserArgs, AsyncDownloadArgs)
            Dim args As New WeatherFeedDownloadCompletedEventArgs(dlArgs.UserArgs, Me.ToResponse(e.Response))
            RaiseEvent AsyncDownloadCompleted(Me, args)
        End Sub

        Private Function DownloadUrl(ByVal woeids As IEnumerable(Of Long), ByVal metric As Boolean) As String
            Dim lst As New List(Of Long)
            If woeids IsNot Nothing Then lst.AddRange(woeids)
            If lst.Count > 0 Then
                Dim whereClause As New Text.StringBuilder
                whereClause.Append("w in (")
                For i As Integer = 0 To lst.Count - 1
                    whereClause.Append(""""c)
                    whereClause.Append(lst(i).ToString)
                    whereClause.Append(""""c)
                    If i <> lst.Count - 1 Then whereClause.Append(","c)
                Next
                whereClause.Append(") and ")
                Me.MetricOption(whereClause, metric)

                Return mHelper.YqlUrl("*"c, "weather.woeid", whereClause.ToString, Nothing, False)
            Else
                Throw New ArgumentException("There must be minimum one WOEID.", "woeids")
            End If
        End Function
        Private Function DownloadUrl(ByVal locations As IEnumerable(Of LocationID), ByVal metric As Boolean) As String
            Dim lst As New List(Of LocationID)
            If locations IsNot Nothing Then lst.AddRange(locations)
            If lst.Count > 0 Then
                Dim whereClause As New Text.StringBuilder
                whereClause.Append("location in (")
                For i As Integer = 0 To lst.Count - 1
                    whereClause.Append(""""c)
                    whereClause.Append(lst(i).ID)
                    whereClause.Append(""""c)
                    If i <> lst.Count - 1 Then whereClause.Append(","c)
                Next
                whereClause.Append(") and ")
                Me.MetricOption(whereClause, metric)

                Return mHelper.YqlUrl("*"c, "weather.forecast", whereClause.ToString, Nothing, False)
            Else
                Throw New ArgumentException("There must be minimum one location.", "locations")
            End If
        End Function
        Private Function DownloadUrl(ByVal keywords As IEnumerable(Of String), ByVal opt As ResultCountOptions, ByVal metric As Boolean) As String
            Dim lst As New List(Of String)
            If keywords IsNot Nothing Then lst.AddRange(keywords)
            If lst.Count > 0 Then
                Dim idSearch As New Text.StringBuilder("query in (")
                For i As Integer = 0 To lst.Count - 1
                    idSearch.Append(""""c)
                    idSearch.Append(lst(i))
                    idSearch.Append(""""c)
                    If i <> lst.Count - 1 Then idSearch.Append(","c)
                Next
                idSearch.Append(")")

                Dim feedSearch As New Text.StringBuilder("location in (")
                feedSearch.Append(mHelper.YqlStatement("id", "weather.search", idSearch.ToString, Nothing))
                feedSearch.Append(") and ")
                Me.MetricOption(feedSearch, metric)

                Return mHelper.YqlUrl("*"c, "weather.forecast", feedSearch.ToString, opt, False)
            Else
                Throw New ArgumentException("There must be minimum one keyword.", "keywords")
            End If
        End Function

        Private Overloads Function ToResponse(ByVal resp As Base.StreamResponse) As WeatherFeedResponse
            Dim feeds As New List(Of WeatherFeed)
            Dim xmlDoc = mHelper.GetXmlDocument(resp.Result)
            If xmlDoc IsNot Nothing Then
                For Each feedNode As XmlNode In xmlDoc.GetElementsByTagName("channel")
                    Dim feed As WeatherFeed = Me.ToWeatherFeed(feedNode)
                    If feed IsNot Nothing Then feeds.Add(feed)
                Next
            End If
            Return New WeatherFeedResponse(resp.Connection, feeds.ToArray)
        End Function

        Private Sub MetricOption(ByVal sb As Text.StringBuilder, ByVal metric As Boolean)
            sb.Append("u=""")
            If metric Then : sb.Append("c"c)
            Else : sb.Append("f"c)
            End If
            sb.Append(""""c)
        End Sub



        Private Function ToWeatherFeed(ByVal channelNode As XmlNode) As WeatherFeed
            Dim convCulture As New Globalization.CultureInfo("en-US")
            If channelNode IsNot Nothing Then

                Dim defaultFeed As RSS.Feed = mXmlParser.ToFeed(channelNode)
                If defaultFeed IsNot Nothing Then
                    defaultFeed.Items.Clear()
                    Dim feed As New WeatherFeed
                    feed.CopyValues(defaultFeed)
                    For Each prpNode As XmlNode In channelNode.ChildNodes

                        If prpNode.Prefix.ToLower = "yweather" Then
                            Select Case prpNode.LocalName.ToLower
                                Case "location"
                                    feed.Location = New LocationInfo
                                    For Each att As XmlAttribute In prpNode.Attributes
                                        Select Case att.Name
                                            Case "city" : feed.Location.City = att.InnerText
                                            Case "region" : feed.Location.Region = att.InnerText
                                            Case "country" : feed.Location.Country = att.InnerText
                                        End Select
                                    Next
                                Case "units"
                                    For Each att As XmlAttribute In prpNode.Attributes
                                        Select Case att.Name
                                            Case "temperature"
                                                If att.InnerText.ToLower = "f" Then : feed.TemperatureUnit = DegreesUnit.Fahrenheit
                                                Else : feed.TemperatureUnit = DegreesUnit.Celsius
                                                End If
                                            Case "distance"
                                                If att.InnerText.ToLower = "mi" Then : feed.DistanceUnit = DistanceUnit.Miles
                                                Else : feed.DistanceUnit = DistanceUnit.Kilometer
                                                End If
                                            Case "pressure"
                                                If att.InnerText.ToLower = "in" Then : feed.PressureUnit = PressureUnit.PoundsPerSquareInch
                                                Else : feed.PressureUnit = PressureUnit.Milibars
                                                End If
                                            Case "speed"
                                                If att.InnerText.ToLower = "mph" Then : feed.SpeedUnit = DistanceUnit.Miles
                                                Else : feed.SpeedUnit = DistanceUnit.Kilometer
                                                End If
                                        End Select
                                    Next
                                Case "wind"
                                    feed.Wind = New WindInfo
                                    For Each att As XmlAttribute In prpNode.Attributes
                                        Select Case att.Name
                                            Case "chill" : Integer.TryParse(att.InnerText, feed.Wind.Chill)
                                            Case "direction" : Integer.TryParse(att.InnerText, feed.Wind.Direction)
                                            Case "speed" : Double.TryParse(att.InnerText, Globalization.NumberStyles.Any, convCulture, feed.Wind.Speed)
                                        End Select
                                    Next
                                Case "atmosphere"
                                    feed.Atmosphere = New AtmosphereInfo
                                    For Each att As XmlAttribute In prpNode.Attributes
                                        Select Case att.Name
                                            Case "humidity" : Integer.TryParse(att.InnerText, feed.Atmosphere.HumidityInPercent)
                                            Case "visibility" : Double.TryParse(att.InnerText, Globalization.NumberStyles.Any, convCulture, feed.Atmosphere.VisibilityDistance)
                                            Case "pressure" : Double.TryParse(att.InnerText, Globalization.NumberStyles.Any, convCulture, feed.Atmosphere.Pressure)
                                            Case "rising"
                                                If att.InnerText = "0" Then : feed.Atmosphere.StateOfBarometricPressure = PressureState.Steady
                                                ElseIf att.InnerText = "1" Then : feed.Atmosphere.StateOfBarometricPressure = PressureState.Rising
                                                ElseIf att.InnerText = "2" Then : feed.Atmosphere.StateOfBarometricPressure = PressureState.Falling
                                                End If
                                        End Select
                                    Next
                                Case "astronomy"
                                    feed.Astronomy = New AstronomyInfo
                                    For Each att As XmlAttribute In prpNode.Attributes
                                        Select Case att.Name
                                            Case "sunrise" : Date.TryParse(att.InnerText, convCulture, Globalization.DateTimeStyles.AssumeUniversal, feed.Astronomy.Sunrise)
                                            Case "sunset" : Date.TryParse(att.InnerText, convCulture, Globalization.DateTimeStyles.AssumeUniversal, feed.Astronomy.Sunset)
                                        End Select
                                    Next
                            End Select

                        ElseIf prpNode.Name.ToLower = "item" Then

                            Dim defaultItem As RSS.FeedItem = mXmlParser.ToFeedItem(prpNode)
                            If defaultItem IsNot Nothing Then
                                Dim newItem As New WeatherFeedItem
                                newItem.CopyValues(defaultItem)

                                For Each itemNode As XmlNode In prpNode.ChildNodes
                                    If itemNode.Prefix = "yweather" Then
                                        Select Case itemNode.LocalName.ToLower
                                            Case "condition"
                                                newItem.ActualCondition = New ActualCondition
                                                For Each att As XmlAttribute In itemNode.Attributes
                                                    Select Case att.Name.ToLower
                                                        Case "text" : newItem.ActualCondition.Description = att.InnerText
                                                        Case "code"
                                                            Dim i As Integer = 0
                                                            If Integer.TryParse(att.InnerText, i) AndAlso (i <= Convert.ToInt32(ConditionType.Isolated_Thundershowers) Or i = Convert.ToInt32(ConditionType.Not_Available)) Then
                                                                newItem.ActualCondition.Type = DirectCast(i, ConditionType)
                                                            End If
                                                        Case "temp" : Integer.TryParse(att.InnerText, newItem.ActualCondition.Temperature)
                                                        Case "date"
                                                            'Wed, 17 Aug 2011 10:18 pm CEST

                                                            Dim dateStr As String = att.InnerText
                                                            Dim index As Integer = Math.Max(dateStr.LastIndexOf("am"), dateStr.LastIndexOf("pm"))
                                                            If index > 0 Then
                                                                Dim d As Date
                                                                If Date.TryParseExact(att.InnerText.Substring(0, index + 2), "ddd, dd MMM yyyy hh:mm tt", convCulture, Globalization.DateTimeStyles.None, d) Then
                                                                    newItem.ActualCondition.ForecastDate = d
                                                                End If
                                                            End If
                                                    End Select
                                                Next
                                            Case "forecast"
                                                Dim cond As New Forecast
                                                For Each att As XmlAttribute In itemNode.Attributes
                                                    Select Case att.Name.ToLower
                                                        Case "text" : cond.Description = att.InnerText
                                                        Case "code"
                                                            Dim i As Integer = 0
                                                            If Integer.TryParse(att.InnerText, i) AndAlso (i <= Convert.ToInt32(ConditionType.Isolated_Thundershowers) Or i = Convert.ToInt32(ConditionType.Not_Available)) Then
                                                                cond.Type = DirectCast(i, ConditionType)
                                                            End If
                                                        Case "day"
                                                            Select Case att.InnerText.ToLower
                                                                Case "mon" : cond.Day = DayOfWeek.Monday
                                                                Case "tue" : cond.Day = DayOfWeek.Tuesday
                                                                Case "wed" : cond.Day = DayOfWeek.Wednesday
                                                                Case "thu" : cond.Day = DayOfWeek.Thursday
                                                                Case "fri" : cond.Day = DayOfWeek.Friday
                                                                Case "sat" : cond.Day = DayOfWeek.Saturday
                                                                Case "sun" : cond.Day = DayOfWeek.Sunday
                                                            End Select
                                                        Case "date" : Date.TryParse(att.InnerText, convCulture, Globalization.DateTimeStyles.AssumeUniversal, cond.ForecastDate)
                                                        Case "low" : Integer.TryParse(att.InnerText, cond.LowTemperature)
                                                        Case "high" : Integer.TryParse(att.InnerText, cond.HighTemperature)
                                                    End Select
                                                Next
                                                newItem.ForecastConditions.Add(cond)
                                        End Select
                                    ElseIf itemNode.Prefix = "geo" Then
                                        Select Case itemNode.LocalName.ToLower
                                            Case "lat"
                                                Dim d As Double = 0
                                                If Double.TryParse(itemNode.InnerText, Globalization.NumberStyles.Any, convCulture, d) Then
                                                    newItem.Coordinates = New Geo.Coordinates(newItem.Coordinates.Longitude, d)
                                                End If
                                            Case ("long")
                                                Dim d As Double = 0
                                                If Double.TryParse(itemNode.InnerText, Globalization.NumberStyles.Any, convCulture, d) Then
                                                    newItem.Coordinates = New Geo.Coordinates(d, newItem.Coordinates.Latitude)
                                                End If
                                        End Select
                                    End If
                                Next
                                feed.Items.Add(newItem)
                            End If
                        End If

                    Next
                    Return feed
                End If
            End If
            Return Nothing

        End Function


        Private Class AsyncDownloadArgs
            Inherits Base.DownloadEventArgs
            Public WOEIDs() As Long
            Public Locations() As LocationID
            Public Keywords() As String
            Public Options As ResultCountOptions
            Public Sub New(ByVal userArgs As Object)
                MyBase.New(userArgs)
            End Sub
        End Class

    End Class



    Public Class WeatherFeedResponse
        Inherits Base.Response
        Public Overloads ReadOnly Property Result() As WeatherFeed()
            Get
                Return TryCast(MyBase.Result, WeatherFeed())
            End Get
        End Property
        Friend Sub New(ByVal info As Base.ConnectionInfo, ByVal result() As WeatherFeed)
            MyBase.New(info, result)
        End Sub
    End Class



    Public Class WeatherFeedDownloadCompletedEventArgs
        Inherits Base.DownloadCompletedEventArgs
        Public Overloads ReadOnly Property Response() As WeatherFeedResponse
            Get
                Return DirectCast(MyBase.Response, WeatherFeedResponse)
            End Get
        End Property
        Friend Sub New(ByVal userArgs As Object, ByVal resp As WeatherFeedResponse)
            MyBase.New(userArgs, resp)
        End Sub
    End Class


End Namespace
